// Author: Domien Nowicki


#include <assert.h>
#include <stdexcept>
#include "World.h"

// Initialize world
World::World(): floorTexture(-1), ceilingTexture(-1), defaultTexture(-1), changed(false), cameraPlaced(false)
{
	// Create new map and texturemanager
	map = new Map();
	textures = new TextureManager();
}

// Create new world, with user provided map and texturemanager
World::World(Map *nMap, TextureManager *nTextureManager): map(nMap), textures(nTextureManager), floorTexture(-1), ceilingTexture(-1), defaultTexture(-1), changed(false), cameraPlaced(false)
{
}

World::~World()
{
	Clear();
}

void World::Reset()
{
	// Clear everything
	if (textures)
	{
		textures->Clear();
	}

	if (map)
	{
		map->Clear();
		map->DefaultResize();
	}

	// Reset textures
	floorTexture = -1;
	ceilingTexture = -1;
	defaultTexture = -1;

	// Remove camera
	RemoveCamera();

	// And label this world as unchanged
	SetUnchanged();
}

void World::Clear()
{
	// Delete textures and map
	if (textures)
	{
		delete textures;
		textures = 0;
	}

	if (map)
	{
		delete map;
		map = 0;
	}

	// Reset textures
	floorTexture = -1;
	ceilingTexture = -1;
	defaultTexture = -1;

	// Remove camera
	RemoveCamera();
}

World& World::operator=(const World &rhs)
{
	Clear();

	// Copy world

	map = new Map(*rhs.map);
	textures = new TextureManager(*rhs.textures);

	floorTexture = rhs.floorTexture;
	ceilingTexture = rhs.ceilingTexture;
	defaultTexture = rhs.defaultTexture;
	changed = rhs.changed;
	cameraPos = rhs.cameraPos;
	cameraAngle = rhs.cameraAngle;
	cameraPlaced = rhs.cameraPlaced;

	return *this;
}

// Check if a cell is empty
bool World::IsEmptyCell(unsigned int column, unsigned int row) const
{
	return map->IsEmptyCell(column, row);
}

// Place a wall in a cell
void World::SetWallCell(unsigned int column, unsigned int row)
{
	unsigned int cameraMapX = cameraPos.GetIntX();
	unsigned int cameraMapY = cameraPos.GetIntY();

	// Only place wall if not containing a camera
	if (!cameraPlaced || !(cameraMapX == column && cameraMapY == row))
	{
		map->SetWallCell(column, row);

		SetChanged();
	}
}

// Make a cell empty again
void World::SetEmptyCell(unsigned int column, unsigned int row)
{
	map->SetEmptyCell(column, row);

	SetChanged();
}

// Operations for getting default textures (and index)
const Texture& World::GetFloorTexture() const
{
	return GetTexture(floorTexture);
}

unsigned int World::GetDefaultTextureIndex() const
{
	return defaultTexture;
}

unsigned int World::GetFloorTextureIndex() const
{
	return floorTexture;
}

const Texture& World::GetCeilingTexture() const
{
	return GetTexture(ceilingTexture);
}

unsigned int World::GetCeilingTextureIndex() const
{
	return ceilingTexture;
}

// Get the world height (is the same as map height)
unsigned int World::GetHeight() const
{
	return GetMapHeight();
}

// Get the world width (is the same as map width)
unsigned int World::GetWidth() const
{
	return GetMapWidth();
}

// Get the world map height (in cell units)
unsigned int World::GetMapHeight() const
{
	return map->GetRowCount();
}

// Get the world map width (in cell units)
unsigned int World::GetMapWidth() const
{
	return map->GetColumnCount();
}

// Resize the world map
void World::Resize(unsigned int newColumnCount, unsigned int newRowCount)
{
	if (cameraPlaced)
	{
		unsigned int cameraMapX = cameraPos.GetIntX();
		unsigned int cameraMapY = cameraPos.GetIntY();

		// If camera drops off the world, remove it
		if (cameraMapX >= newColumnCount || cameraMapY >= newRowCount)
		{
			RemoveCamera();
		}
	}

	map->Resize(newColumnCount, newRowCount);

	SetChanged();
}

// Place a texture on a cell's wall side
void World::SetTextureCell(unsigned int column, unsigned int row, Cell::Side side, unsigned int tindex)
{
	assert( textures->ValidIndex(tindex) );

	const Texture& texture = GetTexture(tindex);

	map->SetTextureCell(column, row, side, tindex, texture.GetUserColor());

	SetChanged();
}

// Add a texture to the list, and return its index
unsigned int World::AddTexture(const Texture& texture)
{
	unsigned int tindex = textures->AddTexture(texture);

	SetChanged();
	
	return tindex;
}

// Get the texture index for a cell's wall side
unsigned int World::GetTextureCell(unsigned int column, unsigned int row, Cell::Side side) const
{
	// Check if we are "outside" the map, if we are, return the "default" index
	if (column < 0 || column >= GetMapWidth() || row < 0 || row >= GetMapHeight())
		return -1;
	else
		return map->GetTextureCell(column, row, side);
}

// Convert texture index to real texture
const Texture& World::GetTexture(unsigned int tindex) const
{
	return textures->GetTexture(tindex);
}

// Remove a texture from the world (so from both the map and texture list)
void World::RemoveTexture(unsigned int tindex)
{
	assert( textures->ValidIndex(tindex) );


	for(unsigned int y=0; y<GetMapHeight(); y++)
	{
		for(unsigned int x=0; x<GetMapWidth(); x++)
		{
			Cell& cell = map->GetCell(x, y);

			for(unsigned int i=0; i<Cell::SIDECOUNT; i++)
			{
				Cell::Side side = (Cell::Side)i;
				Pixel userColor = cell.GetUserColor(side);
				unsigned int textureIndex = cell.GetTexture(side);

				// By removing a texture, we need to update all texture indices
				textureIndex = CalculateTextureIndex(tindex, textureIndex);

				// If it gets removed, the usercolor needs to become the empty usercolor
				if (-1 == textureIndex)
				{
					userColor.SetTransparent();
				}

				// Update cell with its new texture & usercolor
				cell.SetTexture(side, textureIndex, userColor);
			}
		}
	}

	textures->RemoveTexture(tindex);

	defaultTexture = CalculateTextureIndex(tindex, defaultTexture);
	floorTexture = CalculateTextureIndex(tindex,  floorTexture);
	ceilingTexture = CalculateTextureIndex(tindex, ceilingTexture);

	SetChanged();
}

void World::ChangeUserColor(unsigned int tindex, const Pixel& userColor)
{
	assert( textures->ValidIndex(tindex) );

	// Change user color for texture
	textures->ChangeUserColor(tindex, userColor);

	// Change user color for each cell
	for(unsigned int y=0; y<GetMapHeight(); y++)
	{
		for(unsigned int x=0; x<GetMapWidth(); x++)
		{
			Cell& cell = map->GetCell(x, y);

			for(unsigned int i=0; i<Cell::SIDECOUNT; i++)
			{
				Cell::Side side = (Cell::Side)i;

				if (tindex == cell.GetTexture(side))
				{
					
					cell.SetTexture(side, tindex, userColor);
				}
			}
		}
	}


	SetChanged();
}

// Calculate new texture index, incase we are removing a texture
unsigned int World::CalculateTextureIndex(unsigned int removeTextureIndex, unsigned int tindex)
{
	if (tindex == removeTextureIndex)
		tindex = -1;
	else if (tindex != -1 && removeTextureIndex < tindex)
		tindex--;

	return tindex;
}

void World::SaveToFile(const std::string& fileName)
{
	FILE* file = fopen(fileName.c_str(), "wb");

	SaveTexturesToFile(file);
	SaveMapToFile(file);

	fclose(file);

	SetUnchanged();
}

void World::SaveTexturesToFile(FILE* file)
{
	unsigned long textureTag = TEXTURETAG;
	unsigned long textureCount = textures->GetTextureCount();
	unsigned long dataSize = 0;
	

	// Write section tag
	fwrite(&textureTag, sizeof(unsigned long), 1, file);

	// Remember position of dataSize, this will be overwritten with the real size later
	unsigned long dataPosition = ftell(file);
	fwrite(&dataSize, sizeof(unsigned long), 1, file);
	unsigned long startPosition = ftell(file);

	fwrite(&textureCount, sizeof(unsigned long), 1, file);

	// Write each texture to the file
	for(int k = 0; k < textureCount; k++)
	{
		const Texture& texture = textures->GetTexture(k);
		const Pixel& userColor = texture.GetUserColor();
		
		unsigned long userRgb = userColor.GetRgb();
		unsigned long height = texture.GetHeight();
		unsigned long width = texture.GetWidth();
		unsigned long type = texture.IsSolidColor()? SOLIDTYPE:BITMAPTYPE;

		fwrite(&height, sizeof(unsigned long), 1, file);
		fwrite(&width, sizeof(unsigned long), 1, file);
		fwrite(&userRgb, sizeof(unsigned long), 1, file);
		fwrite(&type, sizeof(unsigned long), 1, file);

		if (type == BITMAPTYPE)
		{
			// Write the RGB data of the entire texture
			for(unsigned long i = 0; i < width; i++)
			{
				for(unsigned long j = 0; j < height; j++)
				{
					unsigned long rgb = texture.GetPixel(i, j).GetRgb();
					fwrite(&rgb, RGBSIZE, 1, file);
				}
			}
		}
		else
		{
			unsigned long solidRgb = texture.GetSolidColor().GetRgb();

			fwrite(&solidRgb, RGBSIZE, 1, file);
		}
	}

	// Now re-write the dataSize with the real size for this section
	unsigned long endPosition = ftell(file);

	dataSize = endPosition - startPosition;

	fseek(file, dataPosition, SEEK_SET);
	fwrite(&dataSize, sizeof(unsigned long), 1, file);
	fseek(file, endPosition, SEEK_SET);
}

void World::SaveMapToFile(FILE* file)
{
	unsigned long mapTag = MAPTAG;
	unsigned long mapWidth = map->GetColumnCount();
	unsigned long mapHeight = map->GetRowCount();

	unsigned long floorTexture = GetFloorTextureIndex();
	unsigned long ceilingTexture = GetCeilingTextureIndex();
	unsigned long defaultTexture = GetDefaultTextureIndex();

	unsigned long camX = cameraPos.GetIntX();
	unsigned long camY = cameraPos.GetIntY();
	unsigned long camAngle = (cameraAngle*16).GetInt();
	unsigned long camPlaced = cameraPlaced?1:0;

	unsigned long dataSize = 0;


	// Write section tag
	fwrite(&mapTag, sizeof(unsigned long), 1, file);

	// Remember position of dataSize, this will be overwritten with the real size later
	unsigned long dataPosition = ftell(file);
	fwrite(&dataSize, sizeof(unsigned long), 1, file);
	unsigned long startPosition = ftell(file);

	fwrite(&mapWidth, sizeof(unsigned long), 1, file);
	fwrite(&mapHeight, sizeof(unsigned long), 1, file);

	fwrite(&defaultTexture, sizeof(unsigned long), 1, file);
	fwrite(&floorTexture, sizeof(unsigned long), 1, file);
	fwrite(&ceilingTexture, sizeof(unsigned long), 1, file);

	fwrite(&camPlaced, sizeof(unsigned long), 1, file);
	fwrite(&camX, sizeof(unsigned long), 1, file);
	fwrite(&camY, sizeof(unsigned long), 1, file);
	fwrite(&camAngle, sizeof(unsigned long), 1, file);


	// Now write each cell's information
	for(unsigned long i = 0; i < mapWidth; i++)
	{
		for(unsigned long j = 0; j < mapHeight; j++)
		{
			const Cell& cell = map->GetCell(i, j);

			unsigned long wall = cell.IsEmpty()? 0 : 1;

			fwrite(&wall, sizeof(unsigned long), 1, file);

			// Write each cell side's texture index
			for(int k = 0; k < Cell::SIDECOUNT; k++)
			{
				unsigned long textureIndex = cell.GetTexture( (Cell::Side) k);

				fwrite(&textureIndex, sizeof(unsigned long), 1, file);
			}
		}
	}

	// Now re-write the dataSize with the real size for this section
	unsigned long endPosition = ftell(file);

	dataSize = endPosition - startPosition;

	fseek(file, dataPosition, SEEK_SET);
	fwrite(&dataSize, sizeof(unsigned long), 1, file);
	fseek(file, endPosition, SEEK_SET);
}


void World::LoadFromFile(const std::string& fileName)
{
	unsigned long tag;
	bool mapLoaded = false, texturesLoaded = false;

	FILE *file = fopen(fileName.c_str(), "rb");

	if (0 == file)
	{
		throw std::runtime_error("Map file does not exist");
	}

	while(fread(&tag, sizeof(unsigned long), 1, file) != 0)
	{
		unsigned long dataSize = 0;

		fread(&dataSize, sizeof(unsigned long), 1, file);
	
		switch(tag)
		{
			case TEXTURETAG:
				texturesLoaded = LoadTexturesFromFile(file);
				break;
			case MAPTAG:
				mapLoaded = LoadMapFromFile(file);
				break;
			
			// If we dont understand this tag, skip to the next section
			default:
				fseek(file, dataSize, SEEK_CUR);
				break;
		}
	}

	fclose(file);

	SetUnchanged();

	if (!texturesLoaded || !mapLoaded)
	{
		throw std::runtime_error("Error while loading map file");
	}

}

bool World::LoadTexturesFromFile(FILE* file)
{
	unsigned long textureCount;
	
	textures->Clear();

	if (fread(&textureCount, sizeof(unsigned long), 1, file) == 0)
		goto error;

	for(int k = 0; k < textureCount; k++)
	{
		unsigned long textureWidth, textureHeight, userRgb, type;

		// Create texture based in these parameters
		if (fread(&textureHeight, sizeof(unsigned long), 1, file) == 0)
			goto error;
		if (fread(&textureWidth, sizeof(unsigned long), 1, file) == 0)
			goto error;
		if (fread(&userRgb, sizeof(unsigned long), 1, file) == 0)
			goto error;
		if (fread(&type, sizeof(unsigned long), 1, file) == 0)
			goto error;

		Pixel userColor(userRgb);

		if (type == BITMAPTYPE)
		{
			Texture texture(textureHeight, textureWidth);

			texture.SetUserColor(userColor);


			// Fill in pixels
			for(unsigned long i = 0; i < textureWidth; i++)
			{
				for(unsigned long j = 0; j < textureHeight; j++)
				{
					unsigned long rgb = 0;

					if (fread(&rgb, RGBSIZE, 1, file) == 0)
						goto error;

					Pixel pixel(rgb);
					pixel.SetOpaque();

					texture.SetPixel(i, j, pixel);
				}
			}

			
			// Add texture to the list
			textures->AddTexture(texture);
		} 
		else
		{
			unsigned long solidRgb = 0;

			if (fread(&solidRgb, RGBSIZE, 1, file) == 0)
				goto error;

			Pixel solidColor(solidRgb);
			solidColor.SetOpaque();

			Texture texture(solidColor);
			texture.SetUserColor(userColor);

			// Add texture to the list
			textures->AddTexture(texture);
		}
	}

	return true;

error:
	textures->Clear();
	return false;
}

bool World::LoadMapFromFile(FILE* file)
{
	unsigned long mapWidth, mapHeight;
	unsigned long defaultTexture = -1, floorTexture = -1, ceilingTexture = -1;
	unsigned long camX, camY, camAngle, camPlaced = 0;


	map->Clear();

	// Read in world parameters
	if (fread(&mapWidth, sizeof(unsigned long), 1, file) == 0)
		goto error;
	if (fread(&mapHeight, sizeof(unsigned long), 1, file) == 0)
		goto error;
	if (fread(&defaultTexture, sizeof(unsigned long), 1, file) == 0)
		goto error;
	if (fread(&floorTexture, sizeof(unsigned long), 1, file) == 0)
		goto error;
	if (fread(&ceilingTexture, sizeof(unsigned long), 1, file) == 0)
		goto error;
	if (fread(&camPlaced, sizeof(unsigned long), 1, file) == 0)
		goto error;
	if (fread(&camX, sizeof(unsigned long), 1, file) == 0)
		goto error;
	if (fread(&camY, sizeof(unsigned long), 1, file) == 0)
		goto error;
	if (fread(&camAngle, sizeof(unsigned long), 1, file) == 0)
		goto error;

	// And now create the world


	SetDefaultTexture(defaultTexture);
	SetCeilingTexture(ceilingTexture);
	SetFloorTexture(floorTexture);

	map->Resize(mapWidth, mapHeight);

	for(unsigned long i = 0; i < mapWidth; i++)
	{
		for(unsigned long j = 0; j < mapHeight; j++)
		{
			unsigned long wall;

			if (fread(&wall, sizeof(unsigned long), 1, file) == 0)
				goto error;
			
			// Cell should be a wall?
			if (wall)
			{
					map->SetWallCell(i, j);
			}

			for(int k = 0; k < Cell::SIDECOUNT; k++)
			{
				unsigned long textureIndex;

				if (fread(&textureIndex, sizeof(unsigned long), 1, file) == 0)
					goto error;

				Pixel userColor = GetTexture(textureIndex).GetUserColor();

				// Give each side of the cell its proper texture
				map->SetTextureCell(i, j, (Cell::Side)k, textureIndex, userColor);
			}
		}
	}

	if (camPlaced)
	{
		PlaceCamera(Vector(camX, camY), ((Scalar)camAngle)/16);
	}
	else
	{
		RemoveCamera();
	}

	return true;

error:
	map->Clear();
	map->DefaultResize();

	return false;
}

void World::SetFloorTexture(unsigned int tindex)
{
	floorTexture = tindex;

	SetChanged();
}

void World::SetCeilingTexture(unsigned int tindex)
{
	ceilingTexture = tindex;

	SetChanged();
}

void World::SetDefaultTexture(unsigned int tindex)
{
	defaultTexture = tindex;

	SetChanged();
}

bool World::HasChanged() const
{
	return changed;
}

bool World::HasPlacedCamera() const
{
	return cameraPlaced;
}

// Set that the world has changed
void World::SetChanged()
{
	changed = true;
}

// Set that the world has not changed
void World::SetUnchanged()
{
	changed = false;
}

void World::PlaceCamera(const Vector& pos, const Scalar& angle)
{
	unsigned int cameraMapX = pos.GetIntX();
	unsigned int cameraMapY = pos.GetIntY();

	// Only place camera on empty cells
	if (IsEmptyCell(cameraMapX, cameraMapY))
	{
		cameraPos = pos;
		cameraAngle = angle;
		cameraPlaced = true;

		SetChanged();
	}
}

void World::RemoveCamera()
{
	cameraPlaced = false;

	SetChanged();
}

const Vector& World::GetCameraPos() const
{
	return cameraPos;
}

const Scalar& World::GetCameraAngle() const
{
	return cameraAngle;
}
